#include "platform.h"
#include <stdarg.h>

#define PrynStartGuiPainterFunction() \
	PrynStartFunction (); \
	PrynGuiPainterWindows *platform; \
	\
	prynAssert (painter, NullArgument); \
	state = painter->pState; \
	platform = (PrynGuiPainterWindows *) painter->pPlatform

#define PrynGuiPainterHdc() 

#define toRGB(COLOR) RGB (prynColorR (COLOR), prynColorG (COLOR), prynColorB (COLOR))

PrynGuiExport (PrynResult, prynGuiPainterCreateForHwnd, (PrynGuiPainter **output, PrynState *state, void *window, void *device, void *rectanglePointer))
{
	PrynStartFunctionHasState ();
	#define DefaultFaceName "MS Sans Serif"
	PrynGuiPainter *painter = 0;
	RECT *rect;
	PrynGuiPainterWindows *platform = 0;
	
	if (output) *output = 0;
	prynAssert (output && state && window && device && rectanglePointer, NullArgument);
	
	prynCall (prynAllocateClearedObject (state, &painter));
	
	rect = (RECT *) rectanglePointer;
	painter->pLeft = rect->left;
	painter->pTop = rect->top;
	painter->pRight = rect->right;
	painter->pBottom = rect->bottom;
	
	painter->pState = state;
	prynCall (prynAllocateClearedObject (state, &platform));
	painter->pPlatform = platform;
	
	platform->device = CreateCompatibleDC (device);
	platform->bitmap = CreateCompatibleBitmap (device, rect->right - rect->left, rect->bottom - rect->top);
	SelectObject (platform->device, platform->bitmap);
	
	platform->window = window;
	platform->windowDevice = device;
	
	prynCall (prynGuiStateWindows (&platform->api, state));
	platform->fontDirty = true;
	platform->font.lfHeight = 16;
	platform->font.lfWeight = 500;
	platform->font.lfCharSet = OEM_CHARSET;
	platform->font.lfOutPrecision = OUT_DEFAULT_PRECIS;
	platform->font.lfClipPrecision = CLIP_DEFAULT_PRECIS;
	platform->font.lfQuality = CLEARTYPE_QUALITY;
	platform->font.lfPitchAndFamily = VARIABLE_PITCH;
	prynCopyMemory (platform->font.lfFaceName, DefaultFaceName, sizeof (DefaultFaceName));
	
	SetBkMode (platform->device, TRANSPARENT);
	SetViewportOrgEx (platform->device, -painter->pLeft, -painter->pTop, 0);
	*output = painter;
	
	PrynEndFunctionAfterFailure ({
		prynFree (state, painter);
		prynFree (state, platform);
	});
}

PrynGuiExport (PrynResult, prynGuiPainterDestroy, (PrynGuiPainter *painter))
{
	PrynGuiPainterWindows *platform;
	int width, height;

	if (!painter || !painter->pState || !painter->pPlatform) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	
	width = painter->pRight - painter->pLeft, height = painter->pBottom - painter->pTop;
	
	SetViewportOrgEx (platform->device, 0, 0, 0);
	BitBlt (platform->windowDevice, painter->pLeft, painter->pTop, width, height, platform->device, 0, 0, SRCCOPY);
	
	if (platform->bitmap)
		DeleteObject (platform->bitmap);
	if (platform->device)
		DeleteDC (platform->device);
	if (platform->brushAllocated)
		DeleteObject (platform->brush);
	if (platform->penAllocated)
		DeleteObject (platform->pen);
	if (platform->penBitmap)
		DeleteObject (platform->penBitmap);
	if (platform->fontObject)
		DeleteObject (platform->fontObject);
	
	prynFree (painter->pState, platform);
	prynFree (painter->pState, painter);
	return PrynResultSuccess;
}

PrynGuiExport (PrynResult, prynGuiPainterTranslate, (PrynGuiPainter *painter, int x, int y))
{
	POINT previous;
	PrynStartGuiPainterFunction ();

	prynAssert (
		GetViewportOrgEx (platform->device, &previous) &&
		SetViewportOrgEx (platform->device, previous.x + x, previous.y + y, 0), ExternalFailure);

	PrynEndFunction ();
}

void cleanFont (PrynGuiPainterWindows *platform)
{
	if (!platform->fontDirty)
		return;
	if (platform->fontObject)
		DeleteObject (platform->fontObject);
	platform->fontObject = CreateFontIndirectA (&platform->font);
	if (platform->fontObject) SelectObject (platform->device, platform->fontObject);
}

PrynGuiExport (PrynResult, prynGuiPainterFillRectangleColor, (PrynGuiPainter *painter, int left, int top, int right, int bottom, PrynColor color))
{
	PrynGuiPainterWindows *platform;
	HBRUSH brush;
	RECT area;
	PrynResult result;

	if (!painter || !painter->pPlatform) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	
	brush = CreateSolidBrush (toRGB (color));
	if (!brush) return PrynResultFailure;
	
	area.left = left;
	area.top = top;
	area.right = right;
	area.bottom = bottom;
	
	result = FillRect (platform->device, &area, brush) ? PrynResultSuccess : PrynResultFailure;
	DeleteObject (brush);
	return result;
}

PrynGuiExport (PrynResult, prynGuiPainterSetBrush, (PrynGuiPainter *painter, const PrynGuiBrush *brush))
{
	PrynGuiPainterWindows *platform;

	if (!painter || !painter->pPlatform) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	
	if (platform->brushAllocated)
		DeleteObject (platform->brush);
	platform->brush = 0;
	platform->brushAllocated = false;
	
	switch (brush ? brush->type : PrynGuiBrushTypeNone)
	{
		case PrynGuiBrushTypeNone:
			if ((platform->brush = GetStockObject (NULL_BRUSH)) == 0
				|| !SelectObject (platform->device, platform->brush))
				return PrynResultFailure;
			return PrynResultSuccess;
			
		case PrynGuiBrushTypeColor:
			platform->brush = CreateSolidBrush (toRGB (brush->value.color));
			if (!platform->brush) return PrynResultFailure;
			platform->brushAllocated = true;
			if (!SelectObject (platform->device, platform->brush)) return PrynResultFailure;
			return PrynResultSuccess;
			
		default:
			return PrynResultBadArgument;
	}
}

PrynGuiExport (PrynResult, prynGuiPainterSetPen, (PrynGuiPainter *painter, const PrynGuiPen *pen))
{
	PrynGuiPainterWindows *platform;
	PrynGuiStateWindows *api;
	PrynGuiPenType type;

	if (!painter || !painter->pPlatform) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	prynGuiStateWindows (&api, painter->pState);
	
	if (platform->penAllocated)
		DeleteObject (platform->pen);
	if (platform->penBitmap)
		DeleteObject (platform->penBitmap);
	
	platform->pen = 0;
	platform->penAllocated = false;
	platform->penBitmap = 0;
	platform->penFake = false;

	type = pen ? pen->type : PrynGuiPenTypeNone;
	
retry:
	switch (type)
	{
		case PrynGuiPenTypeNone:
			if ((platform->pen = GetStockObject (NULL_PEN)) == 0
				|| !SelectObject (platform->device, platform->pen))
				return PrynResultFailure;
			return PrynResultSuccess;
		
		case PrynGuiPenTypeSolid:
			if (platform->api && platform->api->ExtCreatePen)
			{
				LOGBRUSH brush;
				int style = PS_GEOMETRIC;
				
				switch (pen->repeat)
				{
					case PrynGuiPenRepeatSolid: style |= PS_SOLID; break;
					case PrynGuiPenRepeatDash: style |= PS_DASH; break;
				}
				style |= PS_ENDCAP_ROUND, style |= PS_JOIN_MITER;
				
				brush.lbStyle = BS_SOLID;
				brush.lbColor = toRGB (pen->color);
				
				platform->pen = ExtCreatePen (style, pen->width, &brush, 0, 0);
			}
			else
				platform->pen = CreatePen (PS_SOLID, pen->width, toRGB (pen->color));
			
			if (!platform->pen) return PrynResultFailure;
			platform->penAllocated = true;
			if (!SelectObject (platform->device, platform->pen)) return PrynResultFailure;
			return PrynResultSuccess;
			
		case PrynGuiPenTypePattern:
			if (api && api->CreateDIBitmap && api->PlgBlt)
			{
				BITMAPINFOHEADER info;
				size_t imagePitch;
				unsigned char *image;
				HBITMAP bitmap;
				int x, y;
				
				if ((platform->pen = GetStockObject (NULL_PEN)) == 0
					|| !SelectObject (platform->device, platform->pen))
					return PrynResultFailure;
					
				prynClearObject (&info);
				info.biSize = sizeof (info);
				info.biWidth = pen->imageWidth;
				info.biHeight = pen->imageHeight;
				info.biPlanes = 1;
				info.biBitCount = 24;
				info.biCompression = BI_RGB;
				
				imagePitch = (pen->imageWidth * 3 + 3) & ~3;
				prynAllocate (painter->pState, imagePitch * pen->imageHeight, (void **) &image);
				if (!image) return PrynResultOutOfMemory;
				
				for (y = 0; y < pen->imageHeight; y ++)
				for (x = 0; x < pen->imageWidth; x ++)
				{
					PrynColor i = pen->image [x + y * pen->imageWidth];
					unsigned char *o = image + x * 3 + y * imagePitch;
					o [0] = prynColorB (i);
					o [1] = prynColorG (i);
					o [2] = prynColorR (i);
				}
				
				bitmap = CreateDIBitmap (platform->device, &info, CBM_INIT, image, (BITMAPINFO *) &info, DIB_RGB_COLORS);
				prynFree (painter->pState, image);
				
				if (bitmap)
				{
					platform->penOffset = pen->offset;
					platform->penBitmap = bitmap;
					platform->penFake = true;
					platform->penBitmapWidth = pen->imageWidth;
					platform->penBitmapHeight = pen->imageHeight;
					platform->penWidth = pen->width;
					return PrynResultSuccess;
				}
			}
			type = PrynGuiPenTypeSolid;
			goto retry;
			
		default:
			return PrynResultBadArgument;
	}
}

PrynGuiExport (PrynResult, prynGuiPainterRectangle, (PrynGuiPainter *painter, int left, int top, int right, int bottom))
{
	PrynGuiPainterWindows *platform;

	if (!painter || !painter->pPlatform) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	
	return Rectangle (platform->device, left, top, right, bottom) ? PrynResultSuccess : PrynResultFailure;
}

PrynGuiExport (PrynResult, prynGuiPainterPrint, (PrynGuiPainter *painter, int x, int y, const PrynString *string))
{
	PrynGuiPainterWindows *platform;

	if (!painter || !painter->pPlatform || !string) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	cleanFont (platform);
	
	return TextOutA (platform->device, x, y, string->pointer, string->length) ? PrynResultSuccess : PrynResultFailure;
}

PrynGuiExport (PrynResult, prynGuiPainterSetTextAlign, (PrynGuiPainter *painter, PrynGuiTextAlign align))
{
	PrynGuiPainterWindows *platform;
	UINT mode = 0;

	if (!painter || !painter->pPlatform) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	
	switch (align & PrynGuiTextAlignHorizontalMask)
	{
		case PrynGuiTextAlignRight: mode |= TA_RIGHT; break;
		case PrynGuiTextAlignCenter: mode |= TA_CENTER; break;
		case PrynGuiTextAlignLeft: default: mode |= TA_LEFT; break;
	}
	
	switch (align & PrynGuiTextAlignVerticalMask)
	{
		case PrynGuiTextAlignTop: default: mode |= TA_TOP; break;
		case PrynGuiTextAlignBottom: mode |= TA_BOTTOM; break;
		case PrynGuiTextAlignBaseLine: mode |= TA_BASELINE; break;
	}
	
	platform->align = align;
	return SetTextAlign (platform->device, mode) ? PrynResultSuccess : PrynResultFailure;
}

PrynGuiExport (PrynResult, prynGuiPainterClip, (PrynGuiPainter *painter, int left, int top, int right, int bottom))
{
	PrynGuiPainterWindows *platform;

	if (!painter || !painter->pPlatform) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	return IntersectClipRect (platform->device, left, top, right, bottom) != ERROR ? PrynResultSuccess : PrynResultFailure;
}

PrynGuiExport (PrynResult, prynGuiPainterClipReset, (PrynGuiPainter *painter))
{
	PrynGuiPainterWindows *platform;

	if (!painter || !painter->pPlatform) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	return SelectClipRgn (platform->device, 0) != ERROR ? PrynResultSuccess : PrynResultFailure;
}

PrynGuiExport (PrynResult, prynGuiPainterTextFits, (PrynGuiPainter *painter, const PrynString *string, int width, size_t *output))
{
	PrynGuiPainterWindows *platform;
	SIZE size;

	if (!painter || !painter->pPlatform || !output || !string) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	
	cleanFont (platform);
	return GetTextExtentExPointA (platform->device, string->pointer, string->length, width, (int *) output, 0, &size) ? PrynResultSuccess : PrynResultFailure;
}

PrynGuiExport (PrynResult, prynGuiPainterMeasureText, (PrynGuiPainter *painter, const PrynString *string, int *width, int *height))
{
	PrynGuiPainterWindows *platform;
	PrynResult result = PrynResultSuccess;
	SIZE size;

	if (!painter || !painter->pPlatform || !width || !height || !string) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	
	cleanFont (platform);
	size.cx = size.cy = 0;
	if (!GetTextExtentExPointA (platform->device, string->pointer, string->length, 0, 0, 0, &size)) result = PrynResultFailure;
	*width = size.cx, *height = size.cy;
	return result;
}

PrynGuiExport (PrynResult, prynGuiPainterTextAlign, (PrynGuiPainter *painter, PrynGuiTextAlign *result))
{
	PrynGuiPainterWindows *platform;

	if (!painter || !painter->pPlatform || !result) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	*result = platform->align;
	return PrynResultSuccess;
}

static bool isSpace (char value)
{
	return value == ' ' || value == '\n' || value == '\r' || value == '\t' || value == '\v';
}

PrynGuiExport (PrynResult, prynGuiPainterTextAdvance, (PrynGuiPainter *painter, int *value))
{
	PrynGuiPainterWindows *platform;
	SIZE size;

	if (!painter || !painter->pPlatform || !value) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	
	cleanFont (platform);
	if (!GetTextExtentPoint32A (platform->device, "X", 1, &size))
		return PrynResultFailure;
	*value = size.cy;
	return PrynResultSuccess;
}

PrynGuiExport (PrynResult, prynGuiPainterFontHeight, (PrynGuiPainter *painter, int height))
{
	PrynGuiPainterWindows *platform;

	if (!painter || !painter->pPlatform) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	platform->fontDirty = true;
	platform->font.lfHeight = height;
	return PrynResultSuccess;
}

PrynGuiExport (PrynResult, prynGuiPainterLineData, (PrynGuiPainter *painter, int pointCount, int *points))
{
	PrynGuiPainterWindows *platform;

	if (!painter || !painter->pPlatform) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	
	if (platform->penFake)
	{
		int pbw = platform->penBitmapWidth, pbh = platform->penBitmapHeight, pw = platform->penWidth, pwh = pw / 2;
		HDC bd = CreateCompatibleDC (platform->device);
		PrynGuiStateWindows *api;
		int o = (platform->penOffset % pbh + pbh) % pbh;
		int index;

		prynGuiStateWindows (&api, painter->pState);
		
		if (!bd) return PrynResultFailure;
		SelectObject (bd, platform->penBitmap);
		
		for (index = 0; index < pointCount - 1; index ++)
		{
			int sx = points [index * 2 + 0], sy = points [index * 2 + 1], ex = points [index * 2 + 2], ey = points [index * 2 + 3];
			int dx = prynAbsolute (ex - sx), dy = prynAbsolute (ey - sy);
			POINT c [3];
			
			if (!dx && !dy)
				continue;
			
			if (ex > sx)
			{
				int x, bh;

				ex += pwh, sx -= pwh - 1, dx += pw - 1;
				for (x = 0; x < dx; x += bh)
				{
					bh = pbh - o;
					if (x + bh > dx) bh = dx - x;
					
					c [0].x = x + sx, c [0].y = sy + pwh;
					c [1].x = x + sx, c [1].y = sy - pwh;
					c [2].x = x + sx + bh, c [2].y = sy + pwh;
					
					api->PlgBlt (platform->device, c, bd, 0, o, pbw, bh, 0, 0, 0);
					
					o = (o + bh) % pbh;
				}
			}
			else if (ex < sx)
			{
				int x, bh;

				ex -= pwh - 1, sx += pwh, dx += pw - 1;
				for (x = 0; x < dx; x += bh)
				{
					bh = pbh - o;
					if (x + bh > dx) bh = dx - x;
					
					c [0].x = sx - x, c [0].y = sy + pwh;
					c [1].x = sx - x, c [1].y = sy - pwh;
					c [2].x = sx - x - bh, c [2].y = sy + pwh;
					
					api->PlgBlt (platform->device, c, bd, 0, o, pbw, bh, 0, 0, 0);
					
					o = (o + bh) % pbh;
				}
			}
			else if (ey > sy)
			{
				int y, bh;

				sx -= 1, ex -= 1;
				ey += pwh - 1, sy -= pwh, dy += pw;
				for (y = 0; y < dy; y += bh)
				{
					bh = pbh - o;
					if (y + bh > dy) bh = dy - y;
					
					c [0].y = sy + y, c [0].x = sx + pwh;
					c [1].y = sy + y, c [1].x = sx - pwh;
					c [2].y = sy + y + bh, c [2].x = sx + pwh;
					
					api->PlgBlt (platform->device, c, bd, 0, o, pbw, bh, 0, 0, 0);
					
					o = (o + bh) % pbh;
				}
			}
			else
			{
				int y, bh;

				sx -= 1, ex -= 1;
				ey -= pwh + 1, sy += pwh - 1, dy += pw;
				for (y = 0; y < dy; y += bh)
				{
					bh = pbh - o;
					if (y + bh > dy) bh = dy - y;
					
					c [0].y = sy - y, c [0].x = sx + pwh;
					c [1].y = sy - y, c [1].x = sx - pwh;
					c [2].y = sy - y - bh, c [2].x = sx + pwh;
					
					api->PlgBlt (platform->device, c, bd, 0, o, pbw, bh, 0, 0, 0);
					
					o = (o + bh) % pbh;
				}
			}
		}
		
		DeleteDC (bd);
		return PrynResultSuccess;
	}
	
	return Polyline (platform->device, (POINT *) points, pointCount) ? PrynResultSuccess : PrynResultFailure;
}

PrynGuiExport (PrynResult, prynGuiPainterCircle, (PrynGuiPainter *painter, int centerX, int centerY, int radius))
{
	PrynGuiPainterWindows *platform;

	if (!painter || !painter->pPlatform) return PrynResultNullArgument;
	platform = (PrynGuiPainterWindows *) painter->pPlatform;
	
	return Ellipse (platform->device, centerX - radius, centerY - radius, centerX + radius, centerY + radius) ? PrynResultSuccess : PrynResultFailure;
}
